using Crux.Core.Dapper.Attributes;
using Crux.Core.Dapper.SqlGenerator;

namespace Crux.Core.Dapper;

public partial class UnitOfWork<TContext> where TContext : DapperContext
{
    /// <inheritdoc/>
    public TEntity Update<TEntity>(
        TEntity entity,
        Action<SqlGenerator<TEntity>>? configure = null) where TEntity : class
    {
        var generator = GetSqlGenerator(configure);

        var sqlQuery = generator.GetUpdate(entity);

        int effectRow = Execute(sqlQuery.GetSql(), sqlQuery.Param);

        if (logger.IsEnabled(LogLevel.Trace))
            logger.LogTrace($"Update EffectRow:{effectRow}");

        return entity;
    }

    /// <inheritdoc/>
    public async Task<TEntity> UpdateAsync<TEntity>(
        TEntity entity,
        Action<SqlGenerator<TEntity>>? configure = null,
        CancellationToken cancellationToken = default) where TEntity : class
    {
        var generator = GetSqlGenerator(configure);

        var sqlQuery = generator.GetUpdate(entity);

        var effectRow = await ExecuteAsync(sqlQuery.GetSql(), sqlQuery.Param, cancellationToken).ConfigureAwait(false);
        if (logger.IsEnabled(LogLevel.Trace))
            logger.LogTrace($"Update EffectRow:{effectRow}");

        return entity;
    }

    /// <inheritdoc/>
    public int Update<TEntity>(
        Expression<Func<TEntity, bool>> where,
        Expression<Func<TEntity, TEntity>> expression,
        Action<SqlGenerator<TEntity>>? configure = null) where TEntity : class
    {
        var generator = GetSqlGenerator(configure);

        var members = new Dictionary<string, object>();

        var memberInitExpression = (MemberInitExpression)expression.Body;
        var bindings = memberInitExpression.Bindings;

        foreach (var binding in bindings)
        {
            if (binding is MemberAssignment assignment)
            {
                var propertyName = assignment.Member.Name;

                Expression memberExpression = assignment.Expression;
#pragma warning disable CS8604 // Possible null reference argument.
                if (memberExpression.NodeType == ExpressionType.Constant)
                {
                    var constantExpression = memberExpression as ConstantExpression;
                    if (constantExpression is null)
                        throw new ArgumentException(
                            "The MemberAssignment expression is not a ConstantExpression.", "updateExpression");
                    if (!members.ContainsKey(propertyName))
                        members.Add(propertyName, constantExpression.Value);
                }
                else
                {
                    LambdaExpression lambda = Expression.Lambda(memberExpression, null);
                    object? value = lambda.Compile().DynamicInvoke();

                    if (!members.ContainsKey(propertyName))
                        members.Add(propertyName, value);
                }
#pragma warning restore CS8604 // Possible null reference argument.
            }
        }

        if (generator.HasUpdatedAt && generator.UpdatedAtProperty.GetCustomAttribute<UpdatedAtAttribute>() is
            { } attribute)
        {
            var offset = attribute.TimeKind == DateTimeKind.Local
                ? new DateTimeOffset(DateTime.Now)
                : new DateTimeOffset(DateTime.UtcNow);
            if (attribute.OffSet != 0)
            {
                offset = offset.ToOffset(TimeSpan.FromHours(attribute.OffSet));
            }

            var name = generator.UpdatedAtProperty!.Name;

            if (!members.ContainsKey(name))
                members.Add(name, offset);
        }

        if (!members.Any())
        {
            throw new ArgumentException("ExpressionIsNotDefined");
        }

        var sqlQuery = generator.GetUpdate(where, members);

        int effectRow = Execute(sqlQuery.GetSql(), sqlQuery.Param);
        if (logger.IsEnabled(LogLevel.Trace))
            logger.LogTrace($"Update EffectRow:{effectRow}");

        return effectRow;
    }
    /// <inheritdoc/>
    public async Task<int> UpdateAsync<TEntity>(
        Expression<Func<TEntity, bool>> where,
        Expression<Func<TEntity, TEntity>> expression,
        Action<SqlGenerator<TEntity>>? configure = null,
        CancellationToken cancellationToken = default) where TEntity : class
    {
        var generator = GetSqlGenerator(configure);

        var members = new Dictionary<string, object>();

        var memberInitExpression = (MemberInitExpression)expression.Body;
        var bindings = memberInitExpression.Bindings;

        foreach (var binding in bindings)
        {
            if (binding is MemberAssignment assignment)
            {
                var propertyName = assignment.Member.Name;
                Expression memberExpression = assignment.Expression;
#pragma warning disable CS8604 // Possible null reference argument.
                if (memberExpression.NodeType == ExpressionType.Constant)
                {
                    var constantExpression = memberExpression as ConstantExpression;
                    if (constantExpression is null)
                        throw new ArgumentException(
                            "The MemberAssignment expression is not a ConstantExpression.", "updateExpression");
                    if (!members.ContainsKey(propertyName))
                        members.Add(propertyName, constantExpression.Value);
                }
                else
                {
                    LambdaExpression lambda = Expression.Lambda(memberExpression, null);
                    object? value = lambda.Compile().DynamicInvoke();

                    if (!members.ContainsKey(propertyName))
                        members.Add(propertyName, value);
                }
#pragma warning restore CS8604 // Possible null reference argument.
            }
        }

        if (generator.HasUpdatedAt && generator.UpdatedAtProperty.GetCustomAttribute<UpdatedAtAttribute>() is
            { } attribute)
        {
            var offset = attribute.TimeKind == DateTimeKind.Local
                ? new DateTimeOffset(DateTime.Now)
                : new DateTimeOffset(DateTime.UtcNow);
            if (attribute.OffSet != 0)
            {
                offset = offset.ToOffset(TimeSpan.FromHours(attribute.OffSet));
            }

            var name = generator.UpdatedAtProperty!.Name;

            if (!members.ContainsKey(name))
                members.Add(name, offset);
        }

        if (!members.Any())
        {
            throw new ArgumentException("ExpressionIsNotDefined");
        }

        var sqlQuery = generator.GetUpdate(where, members);

        int effectRow = await ExecuteAsync(sqlQuery.GetSql(), sqlQuery.Param, cancellationToken).ConfigureAwait(false);
        if (logger.IsEnabled(LogLevel.Trace))
            logger.LogTrace($"Update EffectRow:{effectRow}");

        return effectRow;
    }
    /// <inheritdoc/>
    public int UpdateRange<TEntity>(
        IEnumerable<TEntity> entities,
        Action<SqlGenerator<TEntity>>? configure = null) where TEntity : class
    {
        var generator = GetSqlGenerator(configure);

        if (generator.Provider == SqlProvider.MSSQL)
        {
            int effectRow = 0;
            int totalInstances = entities.Count();

            var properties = generator.SqlProperties.ToList();

            int exceededTimes = (int)Math.Ceiling(totalInstances * properties.Count / 2099d);
            if (exceededTimes > 1)
            {
                int maxAllowedInstancesPerBatch = totalInstances / exceededTimes;
                if (maxAllowedInstancesPerBatch > 1000) maxAllowedInstancesPerBatch = 1000;
                var maxIterationCount = (int)Math.Ceiling((double)totalInstances / (double)maxAllowedInstancesPerBatch);
                for (int i = 0; i <= maxIterationCount; i++)
                {
                    var skips = i * maxAllowedInstancesPerBatch;

                    if (skips >= totalInstances)
                        break;

                    var items = entities.Skip(skips).Take(maxAllowedInstancesPerBatch);
                    var msSqlQueryResult = generator.GetBulkUpdate(items);
                    effectRow += Execute(msSqlQueryResult.GetSql(), msSqlQueryResult.Param);
                }

                if (logger.IsEnabled(LogLevel.Trace))
                    logger.LogTrace($"Update EffectRow:{effectRow}");

                return effectRow;
            }
        }

        {
            var queryResult = generator.GetBulkUpdate(entities);

            var effectRow = Execute(queryResult.GetSql(), queryResult.Param);
            if (logger.IsEnabled(LogLevel.Trace))
                logger.LogTrace($"Update EffectRow:{effectRow}");

            return effectRow;
        }
    }

    /// <inheritdoc/>
    public async Task<int> UpdateRangeAsync<TEntity>(
        IEnumerable<TEntity> entities,
        Action<SqlGenerator<TEntity>>? configure = null,
        CancellationToken cancellationToken = default) where TEntity : class
    {
        var generator = GetSqlGenerator(configure);

        if (generator.Provider == SqlProvider.MSSQL)
        {
            int effectRow = 0;
            int totalInstances = entities.Count();

            var properties = generator.SqlProperties.ToList();

            int exceededTimes = (int)Math.Ceiling(totalInstances * properties.Count / 2099d);
            if (exceededTimes > 1)
            {
                int maxAllowedInstancesPerBatch = totalInstances / exceededTimes;
                if (maxAllowedInstancesPerBatch > 1000) maxAllowedInstancesPerBatch = 1000;
                var maxIterationCount = (int)Math.Ceiling((double)totalInstances / (double)maxAllowedInstancesPerBatch);
                for (int i = 0; i <= maxIterationCount; i++)
                {
                    var skips = i * maxAllowedInstancesPerBatch;

                    if (skips >= totalInstances)
                        break;

                    var items = entities.Skip(skips).Take(maxAllowedInstancesPerBatch);
                    var msSqlQueryResult = generator.GetBulkUpdate(items);
                    effectRow +=
                        await ExecuteAsync(msSqlQueryResult.GetSql(), msSqlQueryResult.Param, cancellationToken)
                            .ConfigureAwait(false);
                }

                if (logger.IsEnabled(LogLevel.Trace))
                    logger.LogTrace($"Update EffectRow:{effectRow}");

                return effectRow;
            }
        }

        {
            var queryResult = generator.GetBulkUpdate(entities);

            var effectRow = await ExecuteAsync(queryResult.GetSql(), queryResult.Param, cancellationToken)
                .ConfigureAwait(false);
            if (logger.IsEnabled(LogLevel.Trace))
                logger.LogTrace($"Update EffectRow:{effectRow}");

            return effectRow;
        }
    }
}